// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:collection';

import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/scheduler/scheduled_message.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/transform_set_parser.dart';
import 'package:analysis_server/src/session_logger/process_id.dart';
import 'package:analysis_server/src/session_logger/session_logger.dart';
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/overlay_file_system.dart';
import 'package:analyzer/instrumentation/instrumentation.dart';
import 'package:analyzer/source/file_source.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/analysis_options/options_file_validator.dart';
import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:analyzer/src/dart/analysis/file_content_cache.dart';
import 'package:analyzer/src/dart/analysis/performance_logger.dart';
import 'package:analyzer/src/dart/analysis/unlinked_unit_store.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/manifest/manifest_validator.dart';
import 'package:analyzer/src/pubspec/pubspec_validator.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/workspace/blaze.dart';
import 'package:analyzer/src/workspace/blaze_watcher.dart';
import 'package:analyzer/src/workspace/pub.dart';
import 'package:analyzer/src/workspace/workspace.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol;
import 'package:analyzer_plugin/utilities/analyzer_converter.dart';
import 'package:path/path.dart' as path;
import 'package:watcher/watcher.dart';
import 'package:yaml/yaml.dart';

/// Enables watching of files generated by Blaze.
// TODO(michalt): This is a temporary flag that we use to disable this
// functionality due its performance issues. We plan to benchmark and optimize
// it and re-enable it everywhere.
// Not private to enable testing.
// NB: If you set this to `false` remember to disable the
// `integration_test/serve/blaze_changes_test.dart`.
var experimentalEnableBlazeWatching = true;

/// Class that maintains a mapping from included/excluded paths to a set of
/// folders that should correspond to analysis contexts.
abstract class ContextManager {
  /// Return the analysis contexts that are currently defined.
  List<AnalysisContext> get analysisContexts;

  /// Get the callback interface used to create, destroy, and update contexts.
  ContextManagerCallbacks get callbacks;

  /// Set the callback interface used to create, destroy, and update contexts.
  set callbacks(ContextManagerCallbacks value);

  /// A table mapping [Folder]s to the [AnalysisDriver]s associated with them.
  Map<Folder, AnalysisDriver> get driverMap;

  /// Return the list of excluded paths (folders and files) most recently passed
  /// to [setRoots].
  List<String> get excludedPaths;

  /// Return the list of included paths (folders and files) most recently passed
  /// to [setRoots].
  List<String> get includedPaths;

  /// Returns owners of files.
  OwnedFiles get ownedFiles;

  /// Disposes and cleans up any analysis contexts.
  Future<void> dispose();

  /// Return the existing analysis context that should be used to analyze the
  /// given [path], or `null` if the [path] is not analyzed in any of the
  /// created analysis contexts.
  DriverBasedAnalysisContext? getContextFor(String path);

  /// Return the [AnalysisDriver] for the "innermost" context whose associated
  /// folder is or contains the given path.  ("innermost" refers to the nesting
  /// of contexts, so if there is a context for path /foo and a context for
  /// path /foo/bar, then the innermost context containing /foo/bar/baz.dart is
  /// the context for /foo/bar.)
  ///
  /// If no driver contains the given path, `null` is returned.
  AnalysisDriver? getDriverFor(String path);

  /// Handle an [event] from a file watcher.
  void handleWatchEvent(WatchEvent event);

  /// Return `true` if the file or directory with the given [path] will be
  /// analyzed in one of the analysis contexts.
  bool isAnalyzed(String path);

  /// Pauses file watchers.
  ///
  /// Throws if watchers are already paused.
  void pauseWatchers();

  /// Rebuild the set of contexts from scratch based on the data last sent to
  /// [setRoots].
  Future<void> refresh();

  /// Unpauses file watchers.
  ///
  /// Throws if watchers are not paused.
  void resumeWatchers();

  /// Change the set of paths which should be used as starting points to
  /// determine the context directories.
  Future<void> setRoots(List<String> includedPaths, List<String> excludedPaths);
}

/// Callback interface used by [ContextManager] to (a) request that contexts be
/// created, destroyed or updated, (b) inform the client when "pub list"
/// operations are in progress, and (c) determine which files should be
/// analyzed.
///
// TODO(paulberry): eliminate this interface, and instead have [ContextManager]
// operations return data structures describing how context state should be
// modified.
abstract class ContextManagerCallbacks {
  /// The analysis server that created this callback object.
  AnalysisServer get analysisServer;

  /// Called after analysis contexts are created, usually when new analysis
  /// roots are set, or after detecting a change that required rebuilding
  /// the set of analysis contexts.
  void afterContextsCreated();

  /// Called after analysis contexts are destroyed.
  void afterContextsDestroyed();

  /// An [event] was processed, so analysis state might be different now.
  void afterWatchEvent(WatchEvent event);

  /// The given [file] was removed.
  void applyFileRemoved(String file);

  /// Sent the given watch [event] to any interested plugins.
  void broadcastWatchEvent(WatchEvent event);

  /// Invoked on every [FileResult] in the analyzer events stream.
  void handleFileResult(FileResult result);

  /// Add listeners to the [driver]. This must be the only listener.
  ///
  // TODO(scheglov): Just pass results in here?
  void listenAnalysisDriver(AnalysisDriver driver);

  /// The `pubspec.yaml` at [path] was added/modified.
  void pubspecChanged(String path);

  /// The `pubspec.yaml` at [path] was removed.
  void pubspecRemoved(String path);

  /// Record error information for the file with the given [path].
  void recordAnalysisErrors(String path, List<protocol.AnalysisError> errors);
}

/// Class that maintains a mapping from included/excluded paths to a set of
/// folders that should correspond to analysis contexts.
class ContextManagerImpl implements ContextManager {
  /// The [OverlayResourceProvider] used to check for the existence of overlays
  /// and to convert paths into [Resource].
  final OverlayResourceProvider resourceProvider;

  /// The manager used to access the SDK that should be associated with a
  /// particular context.
  final DartSdkManager sdkManager;

  /// The path to the package config file override.
  /// If `null`, then the default discovery mechanism is used.
  final String? packagesFile;

  /// The storage for cached results.
  final ByteStore _byteStore;

  /// The cache of file contents shared between context of the collection.
  final FileContentCache _fileContentCache;

  /// The cache of already deserialized unlinked units.
  final UnlinkedUnitStore _unlinkedUnitStore;

  /// The logger used to create analysis contexts.
  final PerformanceLog _performanceLog;

  /// The scheduler used to create analysis contexts, and report status.
  final AnalysisDriverScheduler _scheduler;

  /// The current set of analysis contexts, or `null` if the context roots have
  /// not yet been set.
  AnalysisContextCollectionImpl? _collection;

  /// The context used to work with file system paths.
  path.Context pathContext;

  /// The list of excluded paths (folders and files) most recently passed to
  /// [setRoots].
  @override
  List<String> excludedPaths = <String>[];

  /// The list of included paths (folders and files) most recently passed to
  /// [setRoots].
  @override
  List<String> includedPaths = <String>[];

  /// The instrumentation service used to report instrumentation data.
  final InstrumentationService _instrumentationService;

  /// The session logger.
  final SessionLogger _sessionLogger;

  @override
  ContextManagerCallbacks callbacks = NoopContextManagerCallbacks();

  @override
  final Map<Folder, AnalysisDriver> driverMap =
      HashMap<Folder, AnalysisDriver>();

  /// Subscriptions to watch included resources for changes.
  final List<StreamSubscription<WatchEvent>> watcherSubscriptions = [];

  /// Whether or not the watchers have been paused.
  ///
  /// This occurs when a request like "Fix All" is temporarily using (and
  /// reverting) overlays and we must prevent any external updates.
  ///
  /// Set via [pauseWatchers] and [resumeWatchers].
  bool _watchersPaused = false;

  /// For each folder, stores the subscription to the Blaze workspace so that we
  /// can establish watches for the generated files.
  final blazeSearchSubscriptions =
      <Folder, StreamSubscription<BlazeSearchInfo>>{};

  /// The watcher service running in a separate isolate to watch for changes
  /// to files generated by Blaze.
  ///
  /// Might be `null` if watching Blaze files is not enabled.
  BlazeFileWatcherService? blazeWatcherService;

  /// The subscription to changes in the files watched by [blazeWatcherService].
  ///
  /// Might be `null` if watching Blaze files is not enabled.
  StreamSubscription<List<WatchEvent>>? blazeWatcherSubscription;

  /// For each [Folder] store which files are being watched. This allows us to
  /// clean up when we destroy a context.
  final blazeWatchedPathsPerFolder = <Folder, _BlazeWatchedFiles>{};

  /// Experiments which have been enabled (or disabled) via the
  /// `--enable-experiment` command-line option.
  final List<String> _enabledExperiments;

  /// Whether to enable fine-grained dependencies.
  final bool withFineDependencies;

  /// Information about the current/last queued context rebuild.
  ///
  /// This is used when a new build is requested to cancel any in-progress
  /// rebuild and wait for it to terminate before starting the next.
  final _CancellingTaskQueue _currentContextRebuild = _CancellingTaskQueue();

  ContextManagerImpl(
    this.resourceProvider,
    this.sdkManager,
    this.packagesFile,
    this._enabledExperiments,
    this._byteStore,
    this._fileContentCache,
    this._unlinkedUnitStore,
    this._performanceLog,
    this._scheduler,
    this._instrumentationService,
    this._sessionLogger, {
    required bool enableBlazeWatcher,
    this.withFineDependencies = false,
  }) : pathContext = resourceProvider.pathContext {
    if (enableBlazeWatcher) {
      blazeWatcherService = BlazeFileWatcherService(_instrumentationService);
      blazeWatcherSubscription = blazeWatcherService!.events.listen(
        (events) => _handleBlazeWatchEvents(events),
      );
    }
  }

  @override
  List<AnalysisContext> get analysisContexts =>
      _collection?.contexts.cast<AnalysisContext>() ?? const [];

  @override
  OwnedFiles get ownedFiles {
    return _collection?.ownedFiles ?? OwnedFiles();
  }

  @override
  Future<void> dispose() async {
    await _destroyAnalysisContexts();
  }

  @override
  DriverBasedAnalysisContext? getContextFor(String path) {
    try {
      return _collection?.contextFor(path);
    } on StateError {
      return null;
    }
  }

  @override
  AnalysisDriver? getDriverFor(String path) {
    return getContextFor(path)?.driver;
  }

  @override
  void handleWatchEvent(WatchEvent event) {
    callbacks.broadcastWatchEvent(event);
    _handleWatchEvent(event);
    callbacks.afterWatchEvent(event);
  }

  @override
  bool isAnalyzed(String path) {
    var collection = _collection;
    if (collection == null) {
      return false;
    }

    return collection.contexts.any(
      (context) => context.contextRoot.isAnalyzed(path),
    );
  }

  @override
  void pauseWatchers() {
    if (_watchersPaused) {
      throw StateError('Watchers are already paused');
    }
    for (var subscription in watcherSubscriptions) {
      subscription.pause();
    }
    _watchersPaused = true;
  }

  /// Starts (an asynchronous) rebuild of analysis contexts.
  @override
  Future<void> refresh() async {
    await _createAnalysisContexts();
  }

  @override
  void resumeWatchers() {
    if (!_watchersPaused) {
      throw StateError('Watchers are not paused');
    }
    for (var subscription in watcherSubscriptions) {
      subscription.resume();
    }
    _watchersPaused = false;
  }

  /// Updates the analysis roots and waits for the contexts to rebuild.
  ///
  /// If the roots have not changed, exits early without performing any work.
  @override
  Future<void> setRoots(
    List<String> includedPaths,
    List<String> excludedPaths,
  ) async {
    if (_rootsAreUnchanged(includedPaths, excludedPaths)) {
      return;
    }

    this.includedPaths = includedPaths;
    this.excludedPaths = excludedPaths;

    await _createAnalysisContexts();
  }

  /// Use the given analysis [driver] to analyze the content of the analysis
  /// options file at the given [path].
  void _analyzeAnalysisOptionsYaml(
    AnalysisDriver driver,
    WorkspacePackageImpl? package,
    String path,
  ) {
    var convertedErrors = const <protocol.AnalysisError>[];
    try {
      var file = resourceProvider.getFile(path);
      var analysisOptions = driver.getAnalysisOptionsForFile(file);
      var content = file.readAsStringSync();
      var lineInfo = LineInfo.fromContent(content);
      var sdkVersionConstraint = (package is PubPackage)
          ? package.sdkVersionConstraint
          : null;
      var errors = analyzeAnalysisOptions(
        FileSource(file),
        content,
        driver.sourceFactory,
        driver.currentSession.analysisContext.contextRoot.root.path,
        sdkVersionConstraint,
        resourceProvider,
      );
      var converter = AnalyzerConverter();
      convertedErrors = converter.convertAnalysisErrors(
        errors,
        lineInfo: lineInfo,
        options: analysisOptions,
      );
    } catch (exception) {
      // If the file cannot be analyzed, fall through to clear any previous
      // errors.
    }
    callbacks.recordAnalysisErrors(path, convertedErrors);
  }

  /// Use the given analysis [driver] to analyze the content of the
  /// AndroidManifest file at the given [path].
  void _analyzeAndroidManifestXml(AnalysisDriver driver, String path) {
    var convertedErrors = const <protocol.AnalysisError>[];
    try {
      var file = resourceProvider.getFile(path);
      var content = file.readAsStringSync();
      var source = FileSource(file);
      var validator = ManifestValidator(source);
      var lineInfo = LineInfo.fromContent(content);
      var analysisOptions = driver.getAnalysisOptionsForFile(file);
      var errors = validator.validate(
        content,
        analysisOptions.chromeOsManifestChecks,
      );
      var converter = AnalyzerConverter();
      convertedErrors = converter.convertAnalysisErrors(
        errors,
        lineInfo: lineInfo,
        options: analysisOptions,
      );
    } catch (exception) {
      // If the file cannot be analyzed, fall through to clear any previous
      // errors.
    }
    callbacks.recordAnalysisErrors(path, convertedErrors);
  }

  /// Use the given analysis [driver] to analyze the content of yaml files
  /// inside [folder].
  void _analyzeFixDataFolder(
    AnalysisDriver driver,
    Folder folder,
    String packageName,
  ) {
    for (var resource in folder.getChildren()) {
      if (resource is File) {
        if (resource.shortName.endsWith('.yaml')) {
          _analyzeFixDataYaml(driver, resource, packageName);
        }
      } else if (resource is Folder) {
        _analyzeFixDataFolder(driver, resource, packageName);
      }
    }
  }

  /// Use the given analysis [driver] to analyze the content of the
  /// given [File].
  void _analyzeFixDataYaml(
    AnalysisDriver driver,
    File file,
    String packageName,
  ) {
    var convertedErrors = const <protocol.AnalysisError>[];
    try {
      var content = file.readAsStringSync();
      var diagnosticListener = RecordingDiagnosticListener();
      var diagnosticReporter = DiagnosticReporter(
        diagnosticListener,
        FileSource(file),
      );
      var parser = TransformSetParser(diagnosticReporter, packageName);
      parser.parse(content);
      var converter = AnalyzerConverter();
      var analysisOptions = driver.getAnalysisOptionsForFile(file);
      convertedErrors = converter.convertAnalysisErrors(
        diagnosticListener.diagnostics,
        lineInfo: LineInfo.fromContent(content),
        options: analysisOptions,
      );
    } catch (exception) {
      // If the file cannot be analyzed, fall through to clear any previous
      // errors.
    }
    callbacks.recordAnalysisErrors(file.path, convertedErrors);
  }

  /// Use the given analysis [driver] to analyze the content of the pubspec file
  /// at the given [path].
  void _analyzePubspecYaml(AnalysisDriver driver, String path) {
    var convertedErrors = const <protocol.AnalysisError>[];
    try {
      var file = resourceProvider.getFile(path);
      var content = file.readAsStringSync();
      var node = loadYamlNode(content, sourceUrl: file.toUri());
      if (node is! YamlMap) {
        node = YamlMap();
      }
      var analysisOptions = driver.getAnalysisOptionsForFile(file);
      var errors = validatePubspec(
        contents: node,
        source: FileSource(file),
        provider: resourceProvider,
        analysisOptions: analysisOptions,
      );
      var converter = AnalyzerConverter();
      var lineInfo = LineInfo.fromContent(content);
      convertedErrors = converter.convertAnalysisErrors(
        errors,
        lineInfo: lineInfo,
        options: analysisOptions,
      );
    } catch (exception) {
      // If the file cannot be analyzed, fall through to clear any previous
      // errors.
    }
    callbacks.recordAnalysisErrors(path, convertedErrors);
  }

  void _checkForAndroidManifestXmlUpdate(String path) {
    if (file_paths.isAndroidManifestXml(pathContext, path)) {
      var driver = getDriverFor(path);
      if (driver != null) {
        _analyzeAndroidManifestXml(driver, path);
      }
    }
  }

  void _checkForFixDataYamlUpdate(String path) {
    String? extractPackageNameFromPath(String path) {
      String? packageName;
      var pathSegments = pathContext.split(path);
      if (pathContext.basename(path) == file_paths.fixDataYaml &&
          pathSegments.length >= 3) {
        // packageName/lib/fix_data.yaml
        packageName = pathSegments[pathSegments.length - 3];
      } else {
        var fixDataIndex = pathSegments.indexOf(file_paths.fixDataYamlFolder);
        if (fixDataIndex >= 2) {
          // packageName/lib/fix_data/foo/bar/fix.yaml
          packageName = pathSegments[fixDataIndex - 2];
        }
      }
      return packageName;
    }

    if (file_paths.isFixDataYaml(pathContext, path)) {
      var driver = getDriverFor(path);
      if (driver != null) {
        String? packageName = extractPackageNameFromPath(path);
        if (packageName != null) {
          var file = resourceProvider.getFile(path);
          _analyzeFixDataYaml(driver, file, packageName);
        }
      }
    }
  }

  /// Recreates all analysis contexts.
  ///
  /// If an existing rebuild is in progress, it will be cancelled and this
  /// rebuild will occur only once it has exited.
  ///
  /// Returns a [Future] that completes once the requested rebuild completes.
  Future<void> _createAnalysisContexts() {
    /// A helper that performs a context rebuild while monitoring the included
    /// paths for changes until the contexts file watchers are ready.
    ///
    /// If changes are detected during the rebuild, the rebuild will be
    /// restarted.
    Future<void> performContextRebuildGuarded(
      CancellationToken cancellationToken,
    ) async {
      /// A helper that performs the context rebuild and waits for all watchers
      /// to be fully initialized.
      Future<void> performContextRebuild() async {
        await _destroyAnalysisContexts();
        _fileContentCache.invalidateAll();

        var watchers = <ResourceWatcher>[];
        var collection = _collection = AnalysisContextCollectionImpl(
          includedPaths: includedPaths,
          excludedPaths: excludedPaths,
          byteStore: _byteStore,
          drainStreams: false,
          enableIndex: true,
          performanceLog: _performanceLog,
          resourceProvider: resourceProvider,
          scheduler: _scheduler,
          sdkPath: sdkManager.defaultSdkDirectory,
          packagesFile: packagesFile,
          fileContentCache: _fileContentCache,
          unlinkedUnitStore: _unlinkedUnitStore,
          enabledExperiments: _enabledExperiments,
          withFineDependencies: withFineDependencies,
        );

        for (var analysisContext in collection.contexts) {
          var driver = analysisContext.driver;

          callbacks.listenAnalysisDriver(driver);

          var rootFolder = analysisContext.contextRoot.root;
          driverMap[rootFolder] = driver;

          for (var included in analysisContext.contextRoot.included) {
            var watcher = included.watch();
            watchers.add(watcher);
            watcherSubscriptions.add(
              watcher.changes.listen(
                _scheduleWatchEvent,
                onError: _handleWatchInterruption,
              ),
            );
          }

          _watchBlazeFilesIfNeeded(rootFolder, driver);

          for (var file in analysisContext.contextRoot.analyzedFiles()) {
            if (file_paths.isAnalysisOptionsYaml(pathContext, file)) {
              var package = analysisContext.contextRoot.workspace
                  .findPackageFor(file);
              _analyzeAnalysisOptionsYaml(driver, package, file);
            } else if (file_paths.isAndroidManifestXml(pathContext, file)) {
              _analyzeAndroidManifestXml(driver, file);
            } else if (file_paths.isDart(pathContext, file)) {
              driver.addFile(file);
            } else if (file_paths.isPubspecYaml(pathContext, file)) {
              _analyzePubspecYaml(driver, file);
            }
          }

          var packageName = rootFolder.shortName;
          var fixDataYamlFile = rootFolder
              .getChildAssumingFolder('lib')
              .getChildAssumingFile(file_paths.fixDataYaml);
          if (fixDataYamlFile.exists) {
            _analyzeFixDataYaml(driver, fixDataYamlFile, packageName);
          }

          var fixDataFolder = rootFolder
              .getChildAssumingFolder('lib')
              .getChildAssumingFolder(file_paths.fixDataYamlFolder);
          if (fixDataFolder.exists) {
            _analyzeFixDataFolder(driver, fixDataFolder, packageName);
          }
        }

        // Finally, wait for the new contexts watchers to all become ready so we
        // can ensure they will not lose any future events before we continue.
        await Future.wait(watchers.map((watcher) => watcher.ready));
      }

      /// A helper that returns whether a change to the file at [path] should
      /// restart any in-progress rebuild.
      bool shouldRestartBuild(String path) {
        return file_paths.isDart(pathContext, path) ||
            file_paths.isAnalysisOptionsYaml(pathContext, path) ||
            file_paths.isPubspecYaml(pathContext, path) ||
            file_paths.isPackageConfigJson(pathContext, path);
      }

      if (cancellationToken.isCancellationRequested) {
        return;
      }

      // Create temporary watchers before we start the context build so we can
      // tell if any files were modified while waiting for the "real" watchers to
      // become ready and start the process again.
      var temporaryWatchers = includedPaths
          .map((path) => resourceProvider.getResource(path))
          .map((resource) => resource.watch())
          .toList();

      // If any watcher picks up an important change while we're running the
      // rest of this method, we will need to start again.
      var needsBuild = true;
      var temporaryWatcherSubscriptions = temporaryWatchers
          .map(
            (watcher) => watcher.changes.listen(
              (event) {
                if (shouldRestartBuild(event.path)) {
                  needsBuild = true;
                }
              },
              onError: (error, stackTrace) {
                // Errors in the watcher such as "Directory watcher closed
                // unexpectedly" on Windows when the buffer overflows also
                // require that we restarted to be consistent.
                needsBuild = true;
                _instrumentationService.logError(
                  'Temporary watcher error; restarting context build.\n'
                  '$error\n$stackTrace',
                );
              },
            ),
          )
          .toList();

      try {
        // Ensure all watchers are ready before we begin any rebuild.
        await Future.wait(temporaryWatchers.map((watcher) => watcher.ready));

        // Max number of attempts to rebuild if changes.
        var remainingBuilds = 5;
        while (needsBuild && remainingBuilds-- > 0) {
          // Reset the flag, as we'll only need to rebuild if a temporary
          // watcher fires after this point.
          needsBuild = false;

          if (cancellationToken.isCancellationRequested) {
            return;
          }

          // Attempt a context rebuild. This call will wait for all required
          // watchers to be ready before returning.
          await performContextRebuild();
        }
      } finally {
        // Cancel the temporary watcher subscriptions.
        await Future.wait(
          temporaryWatcherSubscriptions.map((sub) => sub.cancel()),
        );
      }

      if (cancellationToken.isCancellationRequested) {
        return;
      }

      callbacks.afterContextsCreated();
    }

    return _currentContextRebuild.queue(performContextRebuildGuarded);
  }

  /// Clean up and destroy the context associated with the given folder.
  void _destroyAnalysisContext(DriverBasedAnalysisContext context) {
    var rootFolder = context.contextRoot.root;
    var watched = blazeWatchedPathsPerFolder.remove(rootFolder);
    if (watched != null) {
      for (var path in watched.paths) {
        blazeWatcherService!.stopWatching(watched.workspace, path);
      }
    }
    blazeSearchSubscriptions.remove(rootFolder)?.cancel();
    driverMap.remove(rootFolder);
  }

  Future<void> _destroyAnalysisContexts() async {
    for (var subscription in watcherSubscriptions) {
      await subscription.cancel();
    }
    watcherSubscriptions.clear();

    var collection = _collection;
    _collection = null;
    if (collection != null) {
      for (var analysisContext in collection.contexts) {
        _destroyAnalysisContext(analysisContext);
      }
      await collection.dispose();
      callbacks.afterContextsDestroyed();
    }
  }

  /// Establishes watch(es) for the Blaze-generated files provided in [folder].
  ///
  /// Whenever the files change, we trigger re-analysis. This allows us to react
  /// to creation/modification of files that were generated by Blaze.
  void _handleBlazeSearchInfo(
    Folder folder,
    String workspace,
    BlazeSearchInfo info,
  ) {
    var blazeWatcherService = this.blazeWatcherService;
    if (blazeWatcherService == null) {
      return;
    }

    var watched = blazeWatchedPathsPerFolder.putIfAbsent(
      folder,
      () => _BlazeWatchedFiles(workspace),
    );
    var added = watched.paths.add(info.requestedPath);
    if (added) blazeWatcherService.startWatching(workspace, info);
  }

  /// Notifies the drivers that a generated Blaze file has changed.
  void _handleBlazeWatchEvents(List<WatchEvent> events) {
    // If a file was created or removed, the URI resolution is likely wrong.
    // Do as for `package_config.json` changes - recreate all contexts.
    if (events
        .map((event) => event.type)
        .any((type) => type == ChangeType.ADD || type == ChangeType.REMOVE)) {
      refresh();
      return;
    }

    // If we have only changes to generated files, notify drivers.
    for (var driver in driverMap.values) {
      for (var event in events) {
        driver.changeFile(event.path);
      }
    }
  }

  /// Handle an [event] from a file watcher.
  void _handleWatchEvent(WatchEvent event) {
    // Figure out which context this event applies to.
    //
    // If a file is explicitly included in one context but implicitly referenced
    // in another context, we will only notify the context that explicitly
    // includes the file (because that's the only context that's watching the
    // file).
    var path = event.path;
    var type = event.type;

    _instrumentationService.logWatchEvent('<unknown>', path, type.toString());
    _sessionLogger.logMessage(
      from: ProcessId.watcher,
      to: ProcessId.server,
      message: {'path': path, 'type': type.toString()},
    );

    var isPubspec = file_paths.isPubspecYaml(pathContext, path);
    if (file_paths.isAnalysisOptionsYaml(pathContext, path) ||
        file_paths.isBlazeBuild(pathContext, path) ||
        file_paths.isPackageConfigJson(pathContext, path) ||
        isPubspec) {
      _createAnalysisContexts().then((_) {
        if (isPubspec) {
          if (type == ChangeType.REMOVE) {
            callbacks.pubspecRemoved(path);
          } else {
            callbacks.pubspecChanged(path);
          }
        }
      });

      return;
    }

    var collection = _collection;
    if (collection != null &&
        file_paths.isDart(pathContext, path) &&
        // If this resource has an overlay, then the change on disk will never
        // affect analysis results so can be skipped. Removing the overlay will
        // re-read the contents from disk.
        !resourceProvider.hasOverlay(path)) {
      for (var analysisContext in collection.contexts) {
        switch (type) {
          case ChangeType.ADD:
            if (analysisContext.contextRoot.isAnalyzed(path)) {
              analysisContext.driver.addFile(path);
            } else {
              analysisContext.driver.changeFile(path);
            }
          case ChangeType.MODIFY:
            analysisContext.driver.changeFile(path);
          case ChangeType.REMOVE:
            analysisContext.driver.removeFile(path);
        }
      }
    }

    switch (type) {
      case ChangeType.ADD:
      case ChangeType.MODIFY:
        _checkForAndroidManifestXmlUpdate(path);
        _checkForFixDataYamlUpdate(path);
      case ChangeType.REMOVE:
        callbacks.applyFileRemoved(path);
    }
  }

  /// On windows, the directory watcher may overflow, and we must recover.
  void _handleWatchInterruption(Object? error, StackTrace stackTrace) {
    // If the watcher failed because the directory does not exist, rebuilding
    // the contexts will result in infinite looping because it will just
    // re-occur.
    // https://github.com/Dart-Code/Dart-Code/issues/4280
    if (error is PathNotFoundException) {
      _instrumentationService.logError(
        'Watcher error; not refreshing contexts '
        'because PathNotFound.\n$error\n$stackTrace',
      );
      return;
    }

    // We've handled the error, so we only have to log it.
    _instrumentationService.logError(
      'Watcher error; refreshing contexts.\n$error\n$stackTrace',
    );
    // TODO(mfairhurst): Optimize this, or perhaps be less complete.
    refresh();
  }

  /// Checks whether the current roots were built using the same paths as
  /// [includedPaths]/[excludedPaths].
  bool _rootsAreUnchanged(
    List<String> includedPaths,
    List<String> excludedPaths,
  ) {
    if (includedPaths.length != this.includedPaths.length ||
        excludedPaths.length != this.excludedPaths.length) {
      return false;
    }
    var existingIncludedSet = this.includedPaths.toSet();
    var existingExcludedSet = this.excludedPaths.toSet();

    return existingIncludedSet.containsAll(includedPaths) &&
        existingExcludedSet.containsAll(excludedPaths);
  }

  /// Schedule the handling of a watch event.
  ///
  /// This places the watch event on the message scheduler's queue so that it
  /// can be processed when we're not in the middle of handling some other
  /// message.
  void _scheduleWatchEvent(WatchEvent event) {
    callbacks.analysisServer.messageScheduler.add(WatcherMessage(event));
  }

  /// Listens to files generated by Blaze that were found or searched for.
  ///
  /// This is handled specially because the files are outside the package
  /// folder, but we still want to watch for changes to them.
  ///
  /// Does nothing if the [analysisDriver] is not in a Blaze workspace.
  void _watchBlazeFilesIfNeeded(Folder folder, AnalysisDriver analysisDriver) {
    if (!experimentalEnableBlazeWatching) return;
    var watcherService = blazeWatcherService;
    if (watcherService == null) return;

    var workspace = analysisDriver.analysisContext?.contextRoot.workspace;
    if (workspace is BlazeWorkspace &&
        !blazeSearchSubscriptions.containsKey(folder)) {
      blazeSearchSubscriptions[folder] = workspace.blazeCandidateFiles.listen(
        (notification) =>
            _handleBlazeSearchInfo(folder, workspace.root, notification),
      );

      var watched = _BlazeWatchedFiles(workspace.root);
      blazeWatchedPathsPerFolder[folder] = watched;
    }
  }
}

class NoopContextManagerCallbacks implements ContextManagerCallbacks {
  @override
  AnalysisServer get analysisServer => throw StateError(
    'The callback object should have been set by the server.',
  );

  @override
  void afterContextsCreated() {}

  @override
  void afterContextsDestroyed() {}

  @override
  void afterWatchEvent(WatchEvent event) {}

  @override
  void applyFileRemoved(String file) {}

  @override
  void broadcastWatchEvent(WatchEvent event) {}

  @override
  void handleFileResult(FileResult result) {}

  @override
  void listenAnalysisDriver(AnalysisDriver driver) {}

  @override
  void pubspecChanged(String pubspecPath) {}

  @override
  void pubspecRemoved(String pubspecPath) {}

  @override
  void recordAnalysisErrors(String path, List<protocol.AnalysisError> errors) {}
}

class _BlazeWatchedFiles {
  final String workspace;
  final paths = <String>{};
  _BlazeWatchedFiles(this.workspace);
}

/// Handles a task queue of tasks that cannot run concurrently.
///
/// Queueing a new task will signal for any in-progress task to cancel and
/// wait for it to complete before starting the new task.
class _CancellingTaskQueue {
  /// A cancellation token for current/last queued task.
  ///
  /// This token is replaced atomically with [_complete] and
  /// together they allow cancelling a task and chaining a new task on
  /// to the end.
  CancelableToken? _cancellationToken;

  /// A [Future] that completes when the current/last queued task finishes.
  ///
  /// This future is replaced atomically with [_cancellationToken] and together
  /// they allow cancelling a task and chaining a new task on to the end.
  Future<void> _complete = Future.value();

  /// Requests that [performTask] is called after first cancelling any
  /// in-progress task and waiting for it to complete.
  ///
  /// Returns a future that completes once the new task has completed.
  Future<void> queue(
    Future<void> Function(CancellationToken cancellationToken) performTask,
  ) {
    // Signal for any in-progress task to cancel.
    _cancellationToken?.cancel();

    // Chain the new task onto the end of any existing one, so the new
    // task never starts until the previous (cancelled) one finishes (which
    // may be by aborting early because of the cancellation signal).
    var token = _cancellationToken = CancelableToken();
    _complete = _complete
        .then((_) => performTask(token))
        .then((_) => _clearTokenIfCurrent(token));

    return _complete;
  }

  /// Clears the current cancellation token if it is [token].
  void _clearTokenIfCurrent(CancelableToken token) {
    if (token == _cancellationToken) {
      _cancellationToken = null;
    }
  }
}
